public virtual class CloudFilesForce {
/**	
 #	Apex Client wrapper for the Rackspace Cloudfiles API
 *	
 *	See Rackspace API documentation
 *	http://www.rackspacecloud.com/files/cf-devguide-20100217.pdf
 *	twitter: @plmcgrn
**/

	public string storageUrl;
	public string cdnManagementUrl;
	public string authtoken;
	public string account;
	public integer returncode {get; set;}
	
	public virtual class RackspaceException extends Exception {}
	public class AuthenticationException extends CloudFilesForce.RackspaceException {}

	public CloudFilesForce() { }

	/** create a new Youtube client with these credentials
		Calls out to checkCreds to validate credentials.  
	 */
	public CloudFilesForce(String authuser, String authkey) {
		HttpRequest req = createLoginRequest(authuser, authkey);
		HttpResponse res = executeAuth(req);
		this.account = authuser;
		system.debug('auth status: ' + this);
	}
	
	/** structured class for a CloudFiles container **/
	public class cfContainer{
		public string name {get; set;}
		public integer count {get; set;}
		public integer size {get; set;}
	}
	
	/** structured class for a CDN container **/
	public class cfCDNContainer{
		public string name {get; set;}
		public boolean cdnEnabled {get; set;}
		public integer ttl {get; set;}
		public boolean logRetention {get; set;}
		public string url {get; set;}
		public string referrerAcl {get; set;}
		public string useragentAcl {get; set;}
	}	
	
	/** structured class for a CloudFiles object **/
	public class cfObject{
		public string name {get; set;}
		public string contenttype {get; set;}
		public string hash {get; set;}
		public integer bytes {get; set;}
		public string lastmodified {get; set;}		
	}	
	
	protected virtual HttpRequest createLoginRequest(String authuser, String authkey){
   		string path = 'https://auth.api.rackspacecloud.com/v1.0';
   		HttpRequest req = new HttpRequest();
     	req.setEndpoint(path);
     	req.setHeader('X-Auth-User', authuser);
     	req.setHeader('X-Auth-Key', authkey);
      	req.setMethod('GET');

      	return req;		
	}
	
	/** create an authed request **/
	protected virtual HttpRequest createAuthedRequest(string method, String path) {
    	string endpoint = this.storageUrl;
    	HttpRequest req = new HttpRequest();
     	req.setEndpoint(endpoint + path);
     	req.setHeader('X-Auth-Token',this.authtoken);
      	req.setMethod(method == null ? 'GET' : method);
      	return req;
	}
	
	/** create an authed CDN request **/
	protected virtual HttpRequest createAuthedCdnRequest(string method, String path) {
    	string endpoint = this.cdnManagementUrl;
    	HttpRequest req = new HttpRequest();
     	req.setEndpoint(endpoint + path);
     	req.setHeader('X-Auth-Token',this.authtoken);
      	req.setMethod(method == null ? 'GET' : method);
      	return req;
	}	
	
	/** send the auth request to the server **/
    protected virtual HttpResponse executeAuth(HttpRequest req) {
      	System.debug(LoggingLevel.FINE, '\n\nReq: \n' + req + '\n\n');
      	HttpResponse res = new http().send(req);
      	String resBody = res.getBody();
      	System.debug(LoggingLevel.FINE, '\n\nResp: \n' + resBody);
      	if (res.getStatusCode() == 401)
      		throw new AuthenticationException(resBody);
      	else if (res.getStatusCode() == 204)
      		setConnectionInfo(res);
      	else if (res.getStatusCode() != 200)      		
      		throw new RackspaceException(resBody + '\n\n' + res.getHeaderKeys());      	
      	return res;
    }
    
	/** send the http request to the server **/
    protected virtual HttpResponse executeAuthedRequest(HttpRequest req) {
      	System.debug(LoggingLevel.FINE, '\n\nReq: \n' + req + '\n\n');
      	HttpResponse res = new http().send(req);
      	String resBody = res.getBody();
      	integer retCode = res.getStatusCode();
      	System.debug(LoggingLevel.FINE, '\n\nResp: \n' + resBody);
      	if (res.getStatusCode() == 401)
      		throw new AuthenticationException(resBody);
      	else if (res.getStatusCode() == 204){
      		this.returncode = 204;
      	}
      	else if (retCode == 201)
      		this.returncode = 201;
     	else if (retCode == 202)
      		this.returncode = 202;    
      	else if (retCode == 409)
      		this.returncode = 409;  
      	else if (retCode == 404)
      		this.returncode = 404;		
      	else if (retCode != 200)      		     		
      		throw new RackspaceException(resBody + '\n\n' + res.getHeaderKeys());      	
      	
      	return res;
    }    
    
    /**	set the endpoints for CDN and Storage URL **/
    protected virtual void setConnectionInfo(HttpResponse res){
    	String[] headers = res.getHeaderKeys();
    	
    	for(string h : headers){
    		if(h != null && h != ''){
    			system.debug(LoggingLevel.DEBUG,'header - ' + h + ':' + res.getHeader(h));
    		}  		
    	}
    	this.storageUrl = res.getHeader('X-Storage-Url');
    	this.cdnManagementUrl = res.getHeader('X-CDN-Management-Url');
    	this.authtoken = res.getHeader('X-Auth-Token');    	    	
    }
    
    /** get a list of containers on the account **/
    public List<CLoudFilesForce.cfContainer> getContainerList(){
    	HttpRequest req = createAuthedRequest('GET', '/?format=xml');
    	HttpResponse res = executeAuthedRequest(req);
    	
    	return parseContainerList(res);    	
    }
    
    /** get a list of CDN containers on the account **/
    public List<CLoudFilesForce.cfCDNContainer> getCdnContainerList(){
    	HttpRequest req = createAuthedCdnRequest('GET', '/?format=xml');
    	HttpResponse res = executeAuthedRequest(req);
    	
    	return parseCdnContainerList(res);    	
    }    
    
    /** get the number and size of account containers **/
    public Map<string,string> getAccountStats(){
    	HttpRequest req = createAuthedRequest('HEAD', '/');
    	HttpResponse res = executeAuthedRequest(req);
    	map<string,string> accountinfo = new map<string,string>();
    	string containercount = res.getHeader('X-Account-Container-Count');
    	string containersize = res.getHeader('X-Account-Bytes-Used');

    	accountinfo.put('ContainerCount', containercount);
    	accountinfo.put('ContainerSize',containersize);
    	
    	return accountinfo;
    }
    
    /** get the number and size of container objects **/
    public Map<string,string> getContainerStats(string container){
    	HttpRequest req = createAuthedRequest('HEAD', '/' + container);
    	HttpResponse res = executeAuthedRequest(req);
    	map<string,string> containerinfo = new map<string,string>();
    	string objectcount = res.getHeader('X-Container-Object-Count');
    	string containersize = res.getHeader('X-Container-Bytes-Used');

    	containerinfo.put('ContainerCount', objectcount);
    	containerinfo.put('ContainerSize',containersize);
    	
    	return containerinfo;
    }	 
    
    /** get info on the specified CDN container **/
    public Map<string, string> getCdnContainerInfo(string container){
    	HttpRequest req = createAuthedCdnRequest('HEAD', '/' + container);
    	HttpResponse res = executeAuthedRequest(req);
    	map<string,string> cinfo = new map<string,string>(); 
    	
    	string cdnEnabled = res.getHeader('X-CDN-Enabled');
    	string cdnUri = res.getHeader('X-CDN-URI');
    	string ttl = res.getHeader('X-TTL');
    	string logRetention = res.getHeader('X-Log-Retention');
    	string useragentacl = res.getHeader('X-User-Agent-ACL');
    	string referreracl = res.getHeader('X-Referrer-ACL');
    	
    	cinfo.put('cdnEnabled',cdnEnabled);
    	cinfo.put('cdnUrl',cdnUri);
    	cinfo.put('ttl',ttl);
    	cinfo.put('logRetention',logRetention);
    	cinfo.put('userAgentACL',useragentacl);
    	cinfo.put('referrerACL',referreracl);
    	
    	return cinfo;	
    }  
    
    /** get the objects in the specified container **/
    public List<CloudFilesForce.cfObject> getObjectsFromContainer(string container){
    	List<CloudFilesForce.cfObject> lst = new List<CloudFilesForce.cfObject>();
    	HttpRequest req = createAuthedRequest('GET', '/' + container + '?format=xml');
    	HttpResponse res = executeAuthedRequest(req);
		
		system.debug(logginglevel.debug,'response: ' + res.getBody());
    	return parseObjectsFromContainer(res);
    }
    
    /** create a container with the specified name **/
    public string createContainer(string containername){
    	HttpRequest req = createAuthedRequest('PUT', '/' + containername);
    	HttpResponse res = executeAuthedRequest(req);
    	
		if(this.returncode == 201)
			return 'created: ' + containername;
		else if(this.returncode == 202)
			return 'container ' + '\'' + containername + '\' already exists';
		else
			return '';	    	    	
    }	
    
    /** create a CDN container with the specified name **/
    public string createCdnContainer(string containername, integer ttl, boolean logRetention, string useragentACL, string referrerACL){
    	HttpRequest req = createAuthedCdnRequest('PUT', '/' + containername);
    	req.setHeader('X-TTL',string.valueof(ttl));
    	req.setHeader('X-Log-Retention',string.valueOf(logRetention));
    	req.setHeader('X-User-Agent-ACL',useragentacl);
    	req.setHeader('X-Referrer-ACL',referrerACL);
    	    	
    	HttpResponse res = executeAuthedRequest(req);    	

		if(this.returncode == 201)
			return 'created: ' + res.getHeader('X-CDN-URI');
			
		else if(this.returncode == 202)
			return 'container ' + '\'' + containername + '\' already exists ' + res.getHeader('X-CDN-URI');
		else
			return '';	    	    	
    }	  
    
    /** replace CDN container metadata for the specified container **/
    public string setCdnContainerInfo(string containername, integer ttl, boolean enabled, boolean logretention){
    	
    	HttpRequest req = createAuthedCdnRequest('POST', '/' + containername);
    	
    	req.setHeader('X-TTL', string.valueOf(ttl));
    	req.setHeader('X-CDN-Enabled',string.valueOf(enabled));
    	req.setHeader('X-Log-Retention',string.valueOf(logretention));
    	    	
    	HttpResponse res = executeAuthedRequest(req);    	
    	integer ret = this.returncode;
    	
    	if(ret == 202)
    		return 'container settings successfully updated';
    	else if(ret == 404)
    		return 'this object was not found';
    	else
    		return '';    	
    }      
    
    /** delete a container with the specified name **/
    public string deleteContainer(string containername){
    	HttpRequest req = createAuthedRequest('DELETE', '/' + containername);
    	HttpResponse res = executeAuthedRequest(req);
    	
		if(this.returncode == 204)
			return 'successfully deleted "' + containername + '"';
		else if(this.returncode == 409)
			return 'container "' + containername + '" is not empty';
		else if(this.returncode == 404)
			return 'container "' + containername + '" does not exist';
		else	
			return '';	    	
    }
    
    /** get metadata about an object as a map of all standard and custom headers**/
    public Map<string, string> getObjectMetadata(string containername, string objectname){
    	Map<string, string> inf = new Map<string, string>();
    	
    	HttpRequest req = createAuthedRequest('HEAD', '/' + containername + '/' + objectname);
    	HttpResponse res = executeAuthedRequest(req); 
    	
    	if(this.returncode == 204){
	    	for(string s : res.getHeaderKeys()){
	    		if(s != '' && s != null){
	    			inf.put(s,res.getHeader(s));
	    		}
	    	}
    	}    	
    	return inf;    	
    }
    
    /** replace custom object metadata for the specified object **/
    public string setObjectMetadata(string containername, string objectname, map<string, string> metadata){
    	
    	HttpRequest req = createAuthedRequest('POST', '/' + containername + '/' + objectname);
    	
    	//iterate through the map, and set headers for each item
    	set<string> keys = metadata.keyset();
    	for(string s : keys){
    		req.setHeader('X-Object-Meta-' + s, metadata.get(s));
    	}
    	    	
    	HttpResponse res = executeAuthedRequest(req);    	
    	integer ret = this.returncode;
    	
    	if(ret == 202)
    		return 'object metadata successfully overwritten';
    	else if(ret == 404)
    		return 'this object was not found';
    	else
    		return '';    	
    }
    
    /** delete the specified object **/
    public string deleteObject(string containername, string objectname){
    	HttpRequest req = createAuthedRequest('DELETE', '/' + containername + '/' + objectname);
    	HttpResponse res = executeAuthedRequest(req);  
    	
    	integer ret = this.returncode;
    	if(ret == 204)
    		return 'object successfully deleted';
    	else if(ret == 404)
    		return 'object not found';
    	else
    		return '';
    }        
    
    /** parse the XML response from getObjectsFromContainer and return a List of objects **/
    private List<CloudFilesForce.cfObject> parseObjectsFromContainer(HttpResponse res){
    	List<CloudFilesForce.cfObject> cfo = new List<CloudFilesForce.cfObject>();
    	Dom.Document doc = res.getBodyDocument();
    	Dom.Xmlnode rootelement = doc.getRootElement();
    	//system.debug(logginglevel.debug,'root: ' + rootelement);
    	Dom.Xmlnode[] children = rootelement.getChildren();
    	//system.debug(logginglevel.debug,'children: ' + children);    	
    	CloudFilesForce.cfObject c = null;
    	string currentnode;
    	for(Dom.Xmlnode x : children){  
    		if(x.getName() == 'object'){
	    		system.debug(logginglevel.debug,'x: ' + x);    			
	    		
	    		c = new CloudFilesForce.cfObject();    				
		    	
		    	for(Dom.XmlNode y : x.getChildElements()){
		    		//system.debug(logginglevel.debug,'y: ' + y);
			    	currentnode = y.getName();
			    	//system.debug(logginglevel.debug,'current: ' + currentnode);
			    	if(currentnode == 'name')
			    		c.name = y.getText();
			    	if(currentnode == 'hash')
			    		c.hash = y.getText();
			    	if(currentnode == 'content_type')
			    		c.contenttype = y.getText();
			    	if(currentnode == 'last_modified')
			    		c.lastmodified = y.getText();
			    	if(currentnode == 'bytes')
			    		c.bytes = integer.valueOf(y.getText());		    			
		    	}
		    	cfo.add(c);    
    		}		
    	}
    	return cfo;
    }
    
    /** parse a container list xml and return a list of strings **/
    private List<CloudFilesForce.cfContainer> parseContainerList(httpresponse res){
		Dom.Document doc = res.getBodyDocument();
		Dom.XmlNode root = doc.getRootElement();    	

    	List<CloudFilesForce.cfContainer> cons = new List<CloudFilesForce.cfContainer>();
    	CloudFilesForce.cfContainer c = null;
    	string currentnode;
    	Dom.XmlNode[] children = root.getChildren();
    	system.debug(logginglevel.debug,'children: ' + children);
    	for(Dom.XmlNode x : children){
    		if(x.getName() == 'container'){
				c = new CloudFilesForce.cfContainer();	    			
				
	    		for(Dom.XmlNode y : x.getChildren()){
	    			system.debug(logginglevel.debug,'y: ' + y);
		    		currentnode = y.getName();
		    		if(currentnode == 'name')
		    			c.name = y.getText();
		    		else if(currentnode == 'count')
		    			c.count = integer.valueOf(y.getText());
		    		else if(currentnode == 'size')
		    			c.size = integer.valueOf(y.getText());		    		
	    		}
	    		cons.add(c);	    		
    		}    		
    	}
    	return cons;    	
    }     	
    
    /** parse a container list xml and return a list of strings **/
    private List<CloudFilesForce.cfCdnContainer> parseCdnContainerList(httpresponse res){
		Dom.Document doc = res.getBodyDocument();
		Dom.XmlNode root = doc.getRootElement();    	

    	List<CloudFilesForce.cfCdnContainer> cons = new List<CloudFilesForce.cfCdnContainer>();
    	CloudFilesForce.cfCdnContainer c = null;
    	string currentnode;
    	Dom.XmlNode[] children = root.getChildren();
    	system.debug(logginglevel.debug,'children: ' + children);
    	for(Dom.XmlNode x : children){
    		if(x.getName() == 'container'){
				c = new CloudFilesForce.cfCdnContainer();	    			
				
	    		for(Dom.XmlNode y : x.getChildren()){
	    			system.debug(logginglevel.debug,'y: ' + y);
		    		currentnode = y.getName();
		    		if(currentnode == 'name')
		    			c.name = y.getText();
		    		else if(currentnode == 'ttl')
		    			c.ttl = integer.valueOf(y.getText());
		    		else if(currentnode == 'cdn_enabled')
		    			c.cdnEnabled = boolean.valueOf(y.getText());
		    		else if(currentnode == 'log_retention')
		    			c.logRetention = boolean.valueOf(y.getText());
		    		else if(currentnode == 'cdn_url')
		    			c.url = y.getText();
		    		else if(currentnode == 'referrer_acl')
		    			c.referreracl = y.getText();
		    		else if(currentnode == 'useragent_acl')
		    			c.useragentAcl = y.getText();	    		
	    		}
	    		cons.add(c);	    		
    		}    		
    	}
    	return cons;    	
    }      
}